/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.bennedum.SimpleMCClient;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Iterator;

/**
 *
 * @author frdfsnlght <frdfsnlght@gmail.com>
 */
public final class Network extends Thread {
    
    private static final int READ_BUFFER_SIZE = 4096;
    private static final long SELECT_INTERVAL = 50;

    
    private Selector selector = null;
    private boolean run = true;
    private long lastPacketSent = 0;
    private Client client;
    private boolean wantOpen = false;
    private boolean wantClose = false;
    private boolean wantWrite = false;
    private SocketChannel channel = null;
    
    public Network() {
    }

    public void connect(Client client) {
        this.client = client;
        wantOpen = true;
        selector.wakeup();
    }
    
    public void disconnect() {
        wantClose = true;
        selector.wakeup();
    }
    
    public void wantWrite() {
        wantWrite = true;
        selector.wakeup();
    }
    
    @Override
    public void run() {
        try {
            selector = Selector.open();
            
            while (run) {

                if (wantOpen) {
                    wantOpen = false;
                    InetSocketAddress address = client.getServerAddress();
                    Main.out("opening %s...", address);
                    try {
                        channel = SocketChannel.open();
                        channel.configureBlocking(false);
                        channel.connect(address);
                        channel.register(selector, SelectionKey.OP_CONNECT);
                    } catch (IOException e) {
                        client.onException(e);
                    }
                }
                
                if (wantClose) {
                    if (client != null) {
                        Main.out("closing...");
                        if (! client.onHasWriteData()) {
                            Client c = client;
                            killClient();
                            c.onClosed();
                        } else
                            wantWrite = true;
                    } else
                        wantClose = false;
                }
                
                if (wantWrite) {
                    wantWrite = false;
                    SelectionKey key = channel.keyFor(selector);
                    if ((key != null) && key.isValid())
                        key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
                }
                
                if (selector.select(SELECT_INTERVAL) > 0) {
                    Iterator keys = selector.selectedKeys().iterator();
                    while (keys.hasNext()) {
                        SelectionKey key = (SelectionKey)keys.next();
                        keys.remove();
                        if (! key.isValid()) continue;
                        else if (key.isConnectable()) onConnect(key);
                        else if (key.isReadable()) onRead(key);
                        else if (key.isWritable()) onWrite(key);
                    }
                }
                if (client != null)
                    client.onIdle();
            }
            
        } catch (IOException ioe) {
            ioe.printStackTrace();
            System.exit(1);
        }
        if (selector != null)
            try {
                selector.close();
            } catch (IOException ioe) {}
    }

    private void killClient() {
        if (channel != null) {
            SelectionKey key = channel.keyFor(selector);
            if (key != null)
                key.cancel();
            try {
                channel.close();
            } catch (IOException e) {}
            channel = null;
        }
        Client c = client;
        client = null;
        wantOpen = wantClose = wantWrite = false;
        c.onKilled();
    }
    
    private void onConnect(SelectionKey key) {
        try {
            if (channel.isConnectionPending())
                channel.finishConnect();
        } catch (IOException e) {
            client.onException(e);
            return;
        }
        key.interestOps(SelectionKey.OP_READ);
        client.onOpened();
    }
    
    private void onRead(SelectionKey key) {
        ByteBuffer buffer = ByteBuffer.allocate(READ_BUFFER_SIZE);
        int numRead = 0;
        while (true) {
            try {
                numRead = channel.read(buffer);
            } catch (IOException e) {
                client.onException(e);
                return;
            }
            //Main.out("read %d", numRead);
            if (numRead <= 0) break;
            client.onReadData(Arrays.copyOfRange(buffer.array(), 0, numRead));
            if (numRead < READ_BUFFER_SIZE) break;
            buffer.clear();
        }
        if (numRead == -1) {
            Client c = this.client;
            killClient();
            c.onClosed();
        }
    }
    
    private void onWrite(SelectionKey key) {
        ByteBuffer buffer;
        byte[] data;
        int numWrote = 0;
        while (true) {
            data = client.onGetWriteData();
            if (data == null) break;
            buffer = ByteBuffer.wrap(data);
            try {
                numWrote = channel.write(buffer);
            } catch (IOException e) {
                client.onException(e);
                return;
            }
            //Main.out("wrote %d", numWrote);
            if (numWrote == data.length) continue;
            client.onPutWriteData(Arrays.copyOfRange(data, numWrote, data.length - 1));
            break;
        }
        if (! client.onHasWriteData()) {
            key.interestOps(SelectionKey.OP_READ);
            if (wantClose) {
                Client c = client;
                killClient();
                c.onClosed();
                return;
            }
            client.onWriteCompleted();
        }
    }
    
}
